<?php
/* --------------------------------------------------------------
   paypal3.php 2021-11-23
   Gambio GmbH
   http://www.gambio.de
   Copyright (c) 2021 Gambio GmbH
   Released under the GNU General Public License (Version 2)
   [http://www.gnu.org/licenses/gpl-2.0.html]
   --------------------------------------------------------------
*/

defined('GM_HTTP_SERVER') or define('GM_HTTP_SERVER', HTTP_SERVER);

class paypal3_ORIGIN
{
    public $code, $title, $description, $enabled;
    public                              $tmpOrders      = true;
    public                              $tmpStatus      = 0;
    public                              $sort_order;
    public                              $info;
    public                              $order_status, $order_status_pending, $order_status_error;
    protected                           $text;
    protected                           $configStorage;
    protected                           $logger;
    protected                           $selection_logo = '';
    
    
    public function __construct()
    {
        $this->logger               = MainFactory::create('PayPalLogger');
        $this->text                 = MainFactory::create('PayPalText');
        $this->configStorage        = MainFactory::create('PayPalConfigurationStorage');
        $this->code                 = 'paypal3';
        $this->enabled              = defined('MODULE_PAYMENT_' . strtoupper($this->code) . '_STATUS')
                                      && filter_var(constant('MODULE_PAYMENT_' . strtoupper($this->code) . '_STATUS'),
                FILTER_VALIDATE_BOOLEAN);
        $this->title                = defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                              . '_TEXT_TITLE_ADMIN') ? constant('MODULE_PAYMENT_'
                                                                                . strtoupper($this->code)
                                                                                . '_TEXT_TITLE_ADMIN') : '';
        $config_button              = '<br><br><a href="' . xtc_href_link('admin.php', 'do=PayPalConfiguration')
                                      . '" class="button" style="margin: auto; background-color: #E30000;">'
                                      . $this->text->get_text('configure') . '</a><br><br>';
        $this->description          = defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                              . '_TEXT_DESCRIPTION') ? (constant('MODULE_PAYMENT_'
                                                                                 . strtoupper($this->code)
                                                                                 . '_TEXT_DESCRIPTION')
                                                                        . ($this->check() ? $config_button : '')) : '';
        $this->sort_order           = defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                              . '_SORT_ORDER') ? constant('MODULE_PAYMENT_' . strtoupper($this->code)
                                                                           . '_SORT_ORDER') : 0;
        $this->info                 = defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                              . '_TEXT_INFO') ? constant('MODULE_PAYMENT_' . strtoupper($this->code)
                                                                          . '_TEXT_INFO') : '';
        $this->order_status         = $this->configStorage->get('orderstatus/completed');
        $this->order_status_pending = $this->configStorage->get('orderstatus/pending');
        $this->order_status_error   = $this->configStorage->get('orderstatus/error');
        
        if (is_object($GLOBALS['order'])) {
            $this->update_status();
        }
        
        // transport abandonment of withdrawal rights across POST/GET change of checkout_confirmation
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $_SESSION['paypal_abandonment_download'] = isset($_POST['abandonment_download']) ? 'true' : 'false';
            $_SESSION['paypal_abandonment_service']  = isset($_POST['abandonment_service']) ? 'true' : 'false';
        } else {
            if (isset($_SESSION['paypal_abandonment_download'])) {
                $_SESSION['abandonment_download'] = $_SESSION['paypal_abandonment_download'];
                if ($_SESSION['paypal_abandonment_download'] === 'true') {
                    $_POST['abandonment_download'] = 'true';
                }
            }
            if (isset($_SESSION['paypal_abandonment_service'])) {
                $_SESSION['abandonment_service'] = $_SESSION['paypal_abandonment_service'];
                if ($_SESSION['paypal_abandonment_service'] === 'true') {
                    $_POST['abandonment_service'] = 'true';
                }
            }
        }
    }
    
    
    public function check()
    {
        if (!isset($this->_check)) {
            $check_query  = xtc_db_query("SELECT `value` FROM `gx_configurations` where `key` = 'configuration/MODULE_PAYMENT_"
                                         . strtoupper($this->code) . "_STATUS'");
            $this->_check = xtc_db_num_rows($check_query);
        }
        
        return $this->_check;
    }
    
    
    public function update_status()
    {
        $order = $GLOBALS['order'];
        
        if (!$this->_isConfigured()) {
            $this->enabled = false;
        }
        
        if (isset($_SESSION['shipping'])
            && is_array($_SESSION['shipping'])
            && $_SESSION['shipping']['id'] === 'selfpickup_selfpickup'
            && (bool)$this->configStorage->get('allow_selfpickup') === false) {
            $this->enabled = false;
        }
        
        if (($this->enabled === true) && ((int)constant('MODULE_PAYMENT_' . strtoupper($this->code) . '_ZONE') > 0)) {
            $check_flag  = false;
            $check_query = xtc_db_query("SELECT `zone_id` FROM " . TABLE_ZONES_TO_GEO_ZONES . " WHERE `geo_zone_id` = '"
                                        . constant('MODULE_PAYMENT_' . strtoupper($this->code) . '_ZONE')
                                        . "' AND zone_country_id = '" . $order->billing['country']['id']
                                        . "' ORDER BY zone_id");
            while ($check = xtc_db_fetch_array($check_query)) {
                if ($check['zone_id'] < 1) {
                    $check_flag = true;
                    break;
                } elseif ($check['zone_id'] === $order->billing['zone_id']) {
                    $check_flag = true;
                    break;
                }
            }
            
            if ($check_flag === false) {
                $this->enabled = false;
            }
        }
    }
    
    
    protected function _isConfigured()
    {
        $isConfigured = false;
        $mode         = $this->configStorage->get('mode');
        $client_id    = $this->configStorage->get('restapi-credentials/' . $mode . '/client_id');
        $secret       = $this->configStorage->get('restapi-credentials/' . $mode . '/secret');
        if (!empty($client_id) && !empty($secret)) {
            $isConfigured = true;
        }
        
        return $isConfigured;
    }
    
    
    public function javascript_validation()
    {
        return false;
    }
    
    
    public function selection()
    {
        if (isset($_GET['paypal']) && $_GET['paypal'] === 'cancel') {
            unset($_SESSION['payment']);
        }
        
        // default values for ECM mode
        $selection = [
            'id'          => $this->code,
            'module'      => defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                     . '_TEXT_TITLE') ? constant('MODULE_PAYMENT_' . strtoupper($this->code)
                                                                 . '_TEXT_TITLE') : '',
            'description' => defined('MODULE_PAYMENT_' . strtoupper($this->code)
                                     . '_TEXT_DESCRIPTION') ? constant('MODULE_PAYMENT_' . strtoupper($this->code)
                                                                       . '_TEXT_DESCRIPTION') : '',
            'fields'      => [],
        ];
        $stateSelector            = $this->_getStateSelector();
        $selection['description'] .= $stateSelector;
        
        $calledFromCheckoutPayment = is_object($GLOBALS['order'])
                                     && strpos($_SERVER['PHP_SELF'], 'checkout_payment') !== false;
        $isShortcutPayment         = isset($_SESSION['paypal_payment'])
                                     && $_SESSION['paypal_payment']['type'] === 'ecs';
        
        if ($isShortcutPayment === true) {
            if (!isset($_SESSION['paypal_payment']['is_guest'])
                && $_SESSION['paypal_payment']['cartID'] !== $_SESSION['cart']->cartID) {
                unset($_SESSION['paypal_payment']);
                $isShortcutPayment = false;
            } else {
                $selection['description'] .= ' <!-- (ECS) -->';
            }
        }
        
        if (isset($_SESSION['ppplus_disabled']) === false
            && $calledFromCheckoutPayment === true
            && $isShortcutPayment === false
            && empty($stateSelector) === true) {
            if ((bool)$this->configStorage->get('use_paypal_plus') === true) {
                $plus_selection = $this->_selectionPayPalPlus();
                $selection      = $plus_selection !== false ? $plus_selection : $selection;
            } else {
                unset($_SESSION['paypal_payment']);
            }
        }
        
        return $selection;
    }
    
    
    protected function _getStateSelector()
    {
        $stateSelector          = '';
        $stateRequiredCountries = explode(',', 'AR,BR,CA,CN,ID,IN,JP,MX,TH,US');
        $stateRequired          = is_array($GLOBALS['order']->delivery['country'])
                                  && in_array($GLOBALS['order']->delivery['country']['iso_code_2'],
                                              $stateRequiredCountries,
                                              true)
                                  && empty($GLOBALS['order']->delivery['zone_id']);
        if ($stateRequired) {
            $countryService  = StaticGXCoreLoader::getService('Country');
            $deliveryCountry = $countryService->getCountryById(new IdType($GLOBALS['order']->delivery['country']['id']));
            if ($countryService->countryHasCountryZones($deliveryCountry)) {
                $countryZones  = $countryService->findCountryZonesByCountryId(new IdType($GLOBALS['order']->delivery['country']['id']));
                $stateSelector .= '<br>' . $this->text->get_text('select_your_state') . ': ';
                $stateSelector .= '<select name="paypal_zone">';
                foreach ($countryZones as $cZone) {
                    if (in_array($GLOBALS['order']->delivery['state'],
                                 [(string)$cZone->getName(), (string)$cZone->getCode()],
                                 true)
                        || in_array($_SESSION['paypal_state'],
                                    [(string)$cZone->getName(), (string)$cZone->getCode()],
                                    true)) {
                        $zoneSelected = ' selected="selected"';
                    } else {
                        $zoneSelected = '';
                    }
                    $stateSelector .= '<option ' . $zoneSelected . ' value="' . (string)$cZone->getId() . '">'
                                      . (string)$cZone->getName() . '</option>';
                }
                $stateSelector .= '</select>';
            }
        }
        
        return $stateSelector;
    }
    
    
    public function _selectionPayPalPlus()
    {
        try {
            /** @var order_ORIGIN $order */
            $order          = isset($GLOBALS['order']) ? $GLOBALS['order'] : new order();
            $processedTotal = $order->info['total'];
            if (isset($_SESSION['paypal_payment'])
                && $_SESSION['paypal_payment']['type'] === 'plus'
                && $_SESSION['paypal_payment']['state'] === 'created'
                && $_SESSION['paypal_payment']['cartID'] === $_SESSION['cart']->cartID
                && $_SESSION['paypal_payment']['billto'] === $_SESSION['billto']
                && $_SESSION['paypal_payment']['processedTotal'] === $processedTotal) {
                $payPalPayment = MainFactory::create('PayPalPayment', $_SESSION['paypal_payment']['id']);
                $this->logger->notice('re-using existing payment resource ' . $_SESSION['paypal_payment']['id']);
            } else {
                $payPalPaymentFactory       = MainFactory::create_object('PayPalPaymentFactory');
                $payPalPayment              = $payPalPaymentFactory->createPaymentFromOrder($GLOBALS['order'], 'plus');
                $_SESSION['paypal_payment'] = [
                    'id'             => $payPalPayment->id,
                    'type'           => 'plus',
                    'state'          => 'created',
                    'cartID'         => $_SESSION['cart']->cartID,
                    'billto'         => $_SESSION['billto'],
                    'processedTotal' => $processedTotal,
                ];
            }
            $approvalUrl  = $payPalPayment->getLink('approval_url');
            $language     = $this->getLocale();
            $preselection = $this->_hasLowestPriority() ? 'paypal' : 'none';
            
            $ppplusConfigJSON = '{
					"approvalUrl": "' . (string)$approvalUrl->href . '",
					"placeholder": "ppplus",
					"mode": "' . $this->configStorage->get('mode') . '",
					"country": "' . $order->billing['country']['iso_code_2'] . '",
					"language": "' . $language . '",
					"preselection": "' . $preselection . '",
					"showPuiOnSandbox": "true",
					"buttonLocation": "outside",
					"disableContinue": function()
					{
						$("#ppplus_continue").css("opacity", "0.1");
						$(\'div.payment_item input[value="paypal3"]\').get(0).checked = false;
					},
					"enableContinue": function()
					{
						$("div.payment_item input[value=paypal3]").trigger("click");
						$("#ppplus_continue").css("opacity", "1.0");
					},
					"onContinue": function()
					{
						$("#ppplus_continue").closest("form").trigger("submit", ["trigger"]);
					},
					"styles":
					{
						"psp": {
							"font-size": "14px",
							"font-family": "Arial,Tahoma,Verdana",
							"color": "#666",
						}
					},
					"thirdPartyPaymentMethods": thirdPartyPayments,
				}';
            $this->logger->debug_notice("PayPal PLUS configuration generated:\n" . $ppplusConfigJSON);
            $tmpPlaceholder = '<div style="text-align: center;"><img src="' . GM_HTTP_SERVER . DIR_WS_CATALOG
                              . 'images/ladebalken.gif"></div>';
            $ppplusSnippet  = '<div id="ppplus" style="width: 100%; min-height: 20px;">' . $tmpPlaceholder . '</div>';
            if ($preselection === 'none') {
                $preselectionSnippet = ' ppp.deselectPaymentMethod(); $("ul.paypal3-plus-checkout li:first").trigger("click"); ';
            } else {
                $preselectionSnippet = ' /* pp+ preselected */ ';
            }
            $ppplusSnippet .= sprintf('<script> var ppp; var initPPP = function(thirdPartyPayments) { ppp = PAYPAL.apps.PPP(%s); %s}; </script>',
                                      $ppplusConfigJSON,
                                      $preselectionSnippet);
            $description   = $ppplusSnippet;
        } catch (Exception $e) {
            $errorMessage = 'Could not instantiate payment for PP+: ' . $e->getMessage();
            $this->logger->notice($errorMessage);
            $_SESSION['ppplus_disabled'] = true;
            xtc_redirect(xtc_href_link('checkout_payment.php', '', 'SSL'));
            
            return false;
        }
        
        $this->logger->notice('Created payment for PP+ payment wall: ' . $_SESSION['paypal_payment']['id']);
        
        $selection = [
            'id'          => $this->code,
            'module'      => '',
            'description' => $description,
            'fields'      => [],
        ];
        
        return $selection;
    }
    
    
    /** determines LocaleCode from session data */
    protected function getLocale()
    {
        $language           = strtolower($_SESSION['language_code']);
        $shop_country_data  = $this->_findCountryByID(STORE_COUNTRY);
        $shop_country       = $shop_country_data['country_iso_2'];
        $addressBookService = StaticGXCoreLoader::getService('AddressBook');
        $billtoAddress      = $addressBookService->findAddressById(new IdType($_SESSION['billto']));
        $billtoCountry      = (string)$billtoAddress->getCountry()->getIso2();
        $ppLocaleFactory    = MainFactory::create('PayPalLocaleFactory');
        $locale             = $ppLocaleFactory->getLocaleByLanguageAndCountry($language, $billtoCountry);
        if (empty($locale)) {
            $locale = $language . '_' . $shop_country;
        }
        
        return $locale;
    }
    
    
    protected function _hasLowestPriority()
    {
        $integratedModules = [];
        if ($this->configStorage->get('thirdparty_payments/invoice/mode') === 'paywall') {
            $integratedModules[] = '\'configuration/MODULE_PAYMENT_INVOICE_STATUS\'';
        }
        if ($this->configStorage->get('thirdparty_payments/cod/mode') === 'paywall') {
            $integratedModules[] = '\'configuration/MODULE_PAYMENT_COD_STATUS\'';
        }
        if ($this->configStorage->get('thirdparty_payments/moneyorder/mode') === 'paywall') {
            $integratedModules[] = '\'configuration/MODULE_PAYMENT_MONEYORDER_STATUS\'';
        }
        if ($this->configStorage->get('thirdparty_payments/eustandardtransfer/mode') === 'paywall') {
            $integratedModules[] = '\'configuration/MODULE_PAYMENT_EUSTANDARDTRANSFER_STATUS\'';
        }
        if ($this->configStorage->get('thirdparty_payments/cash/mode') === 'paywall') {
            $integratedModules[] = '\'configuration/MODULE_PAYMENT_CASH_STATUS\'';
        }
        
        $minSortOrderQuery = "SELECT
                CAST(`conf`.`value` AS DECIMAL(20,5)) AS `decvalue`,
                `conf`.`value`,
                `conf`.`key`
            FROM
                `gx_configurations` `conf`
            WHERE
                `key` IN (
                    SELECT
                        REPLACE(`key`, '_STATUS', '_SORT_ORDER') AS `module_code`
                    FROM
                        `gx_configurations` `c`
                    WHERE
                        `key` LIKE 'module_payment_%_status' AND
                        `value` = 'True' AND
                        `key` NOT IN (:integrated_modules)
                )
			ORDER BY
				`decvalue` ASC
			LIMIT 1
            ";
        
        $integratedModulesList = empty($integratedModules) ? '\'\'' : implode(', ', $integratedModules);
        $minSortOrderQuery     = strtr($minSortOrderQuery,
                                       [
                                           ':integrated_modules' => $integratedModulesList,
                                       ]);
        $minSortOrderResult    = xtc_db_query($minSortOrderQuery);
        $minSortOrder          = 0;
        while ($row = xtc_db_fetch_array($minSortOrderResult)) {
            $minSortOrder = (double)$row['`value`'];
        }
        $hasLowestPriority = (double)constant('MODULE_PAYMENT_' . strtoupper($this->code) . '_SORT_ORDER')
                             <= $minSortOrder;
        
        return $hasLowestPriority;
    }
    
    
    protected function _findCountryByID($country_id)
    {
        $query        = "SELECT * FROM `countries` WHERE `countries_id` = ':countries_id'";
        $query        = strtr($query, [':countries_id' => (int)$country_id]);
        $result       = xtc_db_query($query);
        $country_data = false;
        while ($row = xtc_db_fetch_array($result)) {
            $country_data = $row;
        }
        
        return $country_data;
    }
    
    
    public function pre_confirmation_check()
    {
        $confirmation_ok = false;
        
        if (isset($_POST['paypal_zone'])) {
            $countryService           = StaticGXCoreLoader::getService('Country');
            $paypalZone               = $countryService->getCountryZoneById(new IdType((int)$_POST['paypal_zone']));
            $_SESSION['paypal_state'] = (string)$paypalZone->getCode();
        }
        
        if (!isset($_GET['paymentId'])) {
            if ((int)gm_get_conf('GM_CHECK_CONDITIONS') !== 1) {
                $_SESSION['conditions'] = 'true/not_reqd';
            }
            if ((int)gm_get_conf('GM_CHECK_WITHDRAWAL') !== 1) {
                $_SESSION['withdrawal'] = 'true/not_reqd';
            }
            if (!empty($_SESSION['paypal_payment']['payer_id'])
                && in_array($_SESSION['paypal_payment']['type'], ['ecs', 'ecm'], true)) {
                $confirmation_ok = true;
            } elseif ($_SESSION['paypal_payment']['type'] === 'plus') {
                // perform last-second updates
                try {
                    if ($_SESSION['cart']->get_content_type() !== 'virtual') {
                        $this->logger->notice('Updating PLUS payment with shipping/billing address: '
                                              . $_SESSION['paypal_payment']['id']);
                        $paymentFactory = MainFactory::create('PayPalPaymentFactory');
                        $paymentFactory->updatePaymentFromOrder($_SESSION['paypal_payment']['id'], $GLOBALS['order']);
                    }
                    $this->logger->notice('Redirecting to PayPal for Plus payment');
                    echo '<!DOCTYPE html><html><head><script src="https://www.paypalobjects.com/webstatic/ppplus/ppplus.min.js" type="text/javascript"></script></head>';
                    echo '<body><script>PAYPAL.apps.PPP.doCheckout();</script><p>'
                         . $this->text->get_text('redirecting_to_paypal') . '</p></body>';
                    exit;
                } catch (Exception $e) {
                    $errorMessage              = $this->text->get_text('error_updating_payment');
                    $_SESSION['paypal3_error'] = $errorMessage;
                    $this->logger->error($errorMessage . ' (' . $e->getMessage() . ')');
                    unset($_SESSION['payment'], $_SESSION['paypal_payment']);
                    xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
                }
            } else // type == 'ecm'
            {
                $this->logger->notice('Instantiating Payment for ECM mode');
                try {
                    $order_total                = new order_total();
                    $order_total_array          = $order_total->process();
                    $paypalState                = isset($_SESSION['paypal_state']) ? $_SESSION['paypal_state'] : null;
                    $payPalPaymentFactory       = MainFactory::create_object('PayPalPaymentFactory');
                    $payPalPayment              = $payPalPaymentFactory->createPaymentFromOrder($GLOBALS['order'],
                                                                                                'ecm',
                                                                                                $paypalState);
                    $approvalLinkEntry          = $payPalPayment->getLink('approval_url');
                    $approvalUrl                = $approvalLinkEntry->href;
                    $approvalUrl                .= '&LocaleCode=' . $this->getLocale();
                    $_SESSION['paypal_payment'] = [
                        'id'     => $payPalPayment->id,
                        'type'   => 'ecm',
                        'state'  => 'created',
                        'cartID' => $_SESSION['cart']->cartID,
                        'billto' => $_SESSION['billto'],
                    ];
                    # die("<br>\nApproval URL: ".$approvalUrl);
                    $this->logger->notice('ECM payment created with id ' . $payPalPayment->id
                                          . ', redirecting customer to ' . $approvalUrl);
                    xtc_redirect($approvalUrl);
                } catch (Exception $e) {
                    $errorMessage = $this->text->get_text('error_creating_ecm_payment');
                    $this->logger->notice($errorMessage . ': ' . $e->getMessage());
                    $_SESSION['paypal3_error'] = $this->text->get_text('paypal_unavailable');
                    unset($_SESSION['payment']);
                    xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
                }
            }
        }
        
        if (!empty($_GET['paymentId']) && $_GET['paymentId'] === $_SESSION['paypal_payment']['id']
            && (!empty($_GET['PayerID']) || !empty($_SESSION['paypal_payment']['payer_id']))) {
            $this->logger->notice('Payment approved by customer: ' . $_SESSION['paypal_payment']['id'] . ', PayerID: '
                                  . $_GET['PayerID']);
            $confirmation_ok                        = true;
            $_SESSION['paypal_payment']['state']    = 'approved';
            $_SESSION['paypal_payment']['payer_id'] = $_GET['PayerID'];
        }
        
        try {
            MainFactory::create('PayPalPayment', $_SESSION['paypal_payment']['id']);
        } catch (Exception $e) {
            $errorMessage = $this->text->get_text('error_retrieving_payment');
            unset($_SESSION['paypal_payment']);
            $this->logger->notice($errorMessage . ': ' . $e->getMessage());
            $_SESSION['paypal3_error'] = $errorMessage;
            xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
        }
        
        if ($confirmation_ok !== true) {
            $this->logger->notice("pre_confirmation_check failed, GET:\n" . print_r($_GET, true));
            $_SESSION['paypal3_error'] = $this->text->get_text('paypal_unavailable');
            unset($_SESSION['payment'], $_SESSION['paypal_payment']);
            xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
        }
        
        return $confirmation_ok;
    }
    
    
    public function confirmation()
    {
        unset($_SESSION['paypal_payment_instruction_href']);
        $confirmation = [
            'title' => $this->text->get_text('checkout_confirmation_info'),
        ];
        
        return $confirmation;
    }
    
    
    public function refresh()
    {
    }
    
    
    public function process_button()
    {
        $order = $GLOBALS['order'];
        $pb    = '';
        
        return $pb;
    }
    
    
    public function after_process()
    {
        // $GLOBALS['order'], $_SESSION['tmp_oID'], $GLOBALS['order_totals']
        if (!empty($_SESSION['paypal_final_order_status'])) {
            // set orders_status again, in case something has changed it
            $insert_id = (int)$_SESSION['tmp_oID'];
            $this->logger->notice(sprintf('Re-setting order status for %d to %d',
                                          $insert_id,
                                          $_SESSION['paypal_final_order_status']));
            $this->_addOrdersStatusHistoryEntry($insert_id, $_SESSION['paypal_final_order_status'], '');
            unset($_SESSION['paypal_final_order_status']);
        }
    }
    
    
    protected function _addOrdersStatusHistoryEntry($orders_id, $orders_status_id, $comments)
    {
        /** @var OrderWriteService $orderWriteService */
        $orderWriteService = StaticGXCoreLoader::getService('OrderWrite');
        $orderWriteService->updateOrderStatus(new IdType((int)$orders_id),
                                              new IntType($orders_status_id),
                                              new StringType($comments),
                                              new BoolType(false));
    }
    
    
    public function before_process()
    {
        return false;
    }
    
    
    public function payment_action()
    {
        $insert_id = $GLOBALS['insert_id'];
        $order     = new order($insert_id);
        $this->logger->notice('Executing payment ' . $_SESSION['paypal_payment']['id'] . ' for orders_id '
                              . $insert_id);
        
        try {
            $paypalPayment = MainFactory::create('PayPalPayment', $_SESSION['paypal_payment']['id']);
        } catch (Exception $e) {
            $errorMessage = $this->text->get_text('error_retrieving_payment');
            unset($_SESSION['paypal_payment']);
            $this->_addOrdersStatusHistoryEntry($insert_id,
                                                $this->order_status_error,
                                                $errorMessage . "\n" . $e->getMessage());
            $this->logger->notice($errorMessage . ': ' . $e->getMessage());
            $_SESSION['paypal3_error'] = $errorMessage;
            xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
        }
        
        try {
            $paymentFactory = MainFactory::create('PayPalPaymentFactory');
            if ($_SESSION['cart']->get_content_type() !== 'virtual') {
                $this->logger->notice('Updating payment with final shipping/billing address: '
                                      . $_SESSION['paypal_payment']['id']);
                $paymentFactory->updatePaymentFromOrder($_SESSION['paypal_payment']['id'], $GLOBALS['order']);
            }
            $this->logger->notice(sprintf('adding invoice_number to payment %s: %s',
                                          $_SESSION['paypal_payment']['id'],
                                          $order->info['orders_id']));
            $paymentFactory->addInvoiceNumber($_SESSION['paypal_payment']['id'], $order);
            // execute payment
            $isPlus = $_SESSION['paypal_payment']['type'] === 'plus';
            $state  = isset($_SESSION['paypal_state']) ? $_SESSION['paypal_state'] : null;
            $this->_addPaymentToOrder($insert_id, $paypalPayment->id, $this->configStorage->get('mode'));
            try {
                $paypalPayment->execute($_SESSION['paypal_payment']['payer_id'], $order, $isPlus, $state);
            } catch (RestTimeoutException $rte) {
                $this->logger->notice('Timeout when executing payment, reloading payment: ' . $paypalPayment->id);
                sleep(5);
                $paypalPayment = MainFactory::create('PayPalPayment', $paypalPayment->id);
                if (!empty($paypalPayment->transactions[0])) {
                    $resourceZero = (array)$paypalPayment->transactions[0]->related_resources[0];
                    $transaction  = array_pop($resourceZero);
                    $this->logger->notice('Transaction state after timed-out execute: ' . (string)$transaction->state);
                    if ((string)$transaction->state !== 'completed') {
                        throw new Exception('Payment not completed after timeout in execute call');
                    }
                }
            } catch (PayPalPayerActionRequiredException $payerActionRequiredException) {
                $this->_addOrdersStatusHistoryEntry($insert_id, $this->order_status_pending, "PAYER_ACTION_REQUIRED");
                $returnUrl   = xtc_href_link('checkout_process.php');
                $redirectUrl = str_replace('redirect_uri=',
                                           'redirect_uri=' . $returnUrl,
                                           $payerActionRequiredException->getRedirectUrl());
                $this->logger->notice("Could not execute payment, PAYER_ACTION_REQUIRED, redirecting to {$redirectUrl}");
                $this->logger->debug_notice("Could not execute payment, PAYER_ACTION_REQUIRED, redirecting to {$redirectUrl}");
                xtc_redirect($redirectUrl);
            }
            try {
                $this->logger->notice('Reloading payment to find payment instruction: ' . $paypalPayment->id);
                $paypalPayment = MainFactory::create('PayPalPayment', $paypalPayment->id);
            } catch (Exception $e) {
                $this->logger->notice('Error reloading payment: ' . $e->getMessage());
            }
            $paymentInstruction = $paypalPayment->payment_instruction;
            if ($paymentInstruction !== null) {
                $this->logger->notice(sprintf('storing payment instruction for order %d in database',
                                              $order->info['orders_id']));
                $this->_storePaymentInstruction($paymentInstruction, $order->info['orders_id']);
            }
            
            $resourceZero     = (array)$paypalPayment->transactions[0]->related_resources[0];
            $transaction      = array_pop($resourceZero);
            $transactionState = strtolower((string)$transaction->state);
            $this->logger->notice('Transaction state after execute: ' . $transactionState);
            
            $final_order_status = $this->order_status;
            if ($this->order_status_pending && in_array($transactionState, ['pending', 'authorized'], true)) {
                $final_order_status = $this->order_status_pending;
            }
            
            $status_comments = '';
            $status_comments .= $this->text->get_text('checkout_mode') . ': '
                                . strtoupper($_SESSION['paypal_payment']['type']);
            if (isset($_SESSION['paypal_payment']['is_guest']) && $_SESSION['paypal_payment']['is_guest'] == true) {
                $this->logger->notice('Payment ' . $_SESSION['paypal_payment']['id'] . ' was initiated by ECS');
                $status_comments .= "\n" . $this->text->get_text('guest_customer_created_from_ecs_data');
                
                // void session for ECS guests unless cart contains download products
                $isDownload = false;
                /** @var array $products */
                $products = $_SESSION['cart']->get_products();
                foreach ($products as $product) {
                    $isDownload = $product['product_type'] === '2';
                }
                if ($isDownload === false) {
                    $_SESSION['paypal_ecs_logout_required'] = true;
                }
            }
            $this->_addOrdersStatusHistoryEntry($insert_id, $final_order_status, $status_comments);
            $this->logger->notice('Payment ' . $_SESSION['paypal_payment']['id'] . ' executed.');
            $GLOBALS['tmp'] = false;
        } catch (Exception $e) {
            $errorMessage = $this->text->get_text('error_executing_payment');
            unset($_SESSION['paypal_payment']);
            $this->_addOrdersStatusHistoryEntry($insert_id,
                                                $this->order_status_error,
                                                $errorMessage . "\n" . $e->getMessage());
            $this->logger->notice('ERROR executing payment ' . $paypalPayment->id . ' - ' . $e->getMessage());
            
            $_SESSION['paypal3_error'] = $errorMessage;
            xtc_redirect(xtc_href_link('checkout_payment.php', 'payment_error=' . $this->code, 'SSL'));
        }
        
        unset($_SESSION['paypal_payment'], $_SESSION['paypal_state'], $_SESSION['paypal_abandonment_service'], $_SESSION['paypal_abandonment_download']);
    }
    
    
    protected function _addPaymentToOrder($orders_id, $payment_id, $mode)
    {
        $query = 'REPLACE
				INTO `orders_paypal_payments`
			SET
				`orders_id` = \':orders_id\',
				`payment_id` = \':payment_id\',
				`mode` = \':mode\'
			';
        $query = strtr($query,
                       [
                           ':orders_id'  => (int)$orders_id,
                           ':payment_id' => xtc_db_input($payment_id),
                           ':mode'       => xtc_db_input($mode),
                       ]);
        xtc_db_query($query);
    }
    
    
    /**
     * stores payment instruction (for payment upon invoice)
     */
    protected function _storePaymentInstruction(stdClass $paymentInstruction, $orders_id)
    {
        $orders_id = (int)$orders_id;
        if ($orders_id <= 0) {
            throw new Exception('invalid value for orders_id');
        }
        if (isset($paymentInstruction->payment_due_date)) {
            $dueDate = xtc_db_input($paymentInstruction->payment_due_date);
        } else {
            $dueDate = '1000-01-01';
        }
        $insert_query = "INSERT
				INTO `orders_payment_instruction`
			SET
				`orders_id` = ':orders_id',
				`reference` = ':reference',
				`bank_name` = ':bank_name',
				`account_holder` = ':account_holder',
				`iban` = ':iban',
				`bic` = ':bic',
				`value` = ':value',
				`currency` = ':currency',
				`due_date` = ':due_date'";
        $insert_query = strtr($insert_query,
                              [
                                  ':orders_id'      => $orders_id,
                                  ':reference'      => xtc_db_input($paymentInstruction->reference_number),
                                  ':bank_name'      => xtc_db_input($paymentInstruction->recipient_banking_instruction->bank_name),
                                  ':account_holder' => xtc_db_input($paymentInstruction->recipient_banking_instruction->account_holder_name),
                                  ':iban'           => xtc_db_input($paymentInstruction->recipient_banking_instruction->international_bank_account_number),
                                  ':bic'            => xtc_db_input($paymentInstruction->recipient_banking_instruction->bank_identifier_code),
                                  ':value'          => sprintf('%.2f', (double)$paymentInstruction->amount->value),
                                  ':currency'       => xtc_db_input($paymentInstruction->amount->currency),
                                  ':due_date'       => $dueDate,
                              ]);
        xtc_db_query($insert_query);
    }
    
    
    public function get_error()
    {
        $error = false;
        if (isset($_SESSION['paypal3_error'])) {
            $error = ['error' => $_SESSION['paypal3_error']];
            unset($_SESSION['paypal3_error']);
        }
        
        return $error;
    }
    
    
    public function install()
    {
        $config     = $this->_configuration();
        $sort_order = 0;
        foreach ($config as $key => $data) {
            $install_query = "INSERT INTO `gx_configurations` ( `key`, `value`, `sort_order`, `type`, `last_modified`) "
                             . "values ('configuration/MODULE_PAYMENT_" . strtoupper($this->code) . "_" . $key . "', '"
                             . $data['value'] . "', '" . $sort_order . "', '" . addslashes($data['type']) . "', now())";
            xtc_db_query($install_query);
            $sort_order++;
        }
    }
    
    
    public function _configuration()
    {
        $config = [
            'STATUS'     => [
                'value' => 'True',
                'type'  => 'switcher',
            ],
            'SORT_ORDER' => [
                'value' => '-9999',
            ],
            'ALLOWED'    => [
                'value' => '',
            ],
            'ZONE'       => [
                'value' => '0',
                'type'  => 'geo-zone',
            ],
        ];
        
        return $config;
    }
    
    
    public function remove()
    {
        xtc_db_query("DELETE FROM `gx_configurations` WHERE `key` IN ('" . implode("', '", $this->keys()) . "')");
    }
    
    
    /**
     * Determines the module's configuration keys
     *
     * @return array
     */
    public function keys()
    {
        $ckeys = array_keys($this->_configuration());
        $keys  = [];
        foreach ($ckeys as $k) {
            $keys[] = 'configuration/MODULE_PAYMENT_' . strtoupper($this->code) . '_' . $k;
        }
        
        return $keys;
    }
    
    
    public function isInstalled()
    {
        $isInstalled = true;
        foreach ($this->keys() as $key) {
            if (!defined($key)) {
                $isInstalled = false;
            }
        }
        
        return $isInstalled;
    }
    
    
    protected function _initLanguageConstants()
    {
        $constant_names = [
            'MODULE_PAYMENT_PAYPAL3_TEXT_DESCRIPTION',
            'MODULE_PAYMENT_PAYPAL3_TEXT_TITLE',
            'MODULE_PAYMENT_PAYPAL3_TEXT_TITLE_ADMIN',
            'MODULE_PAYMENT_PAYPAL3_TEXT_INFO',
            'MODULE_PAYMENT_PAYPAL3_STATUS_TITLE',
            'MODULE_PAYMENT_PAYPAL3_STATUS_DESC',
            'MODULE_PAYMENT_PAYPAL3_SORT_ORDER_TITLE',
            'MODULE_PAYMENT_PAYPAL3_SORT_ORDER_DESC',
            'MODULE_PAYMENT_PAYPAL3_ZONE_TITLE',
            'MODULE_PAYMENT_PAYPAL3_ZONE_DESC',
            'MODULE_PAYMENT_PAYPAL3_ALLOWED_TITLE',
            'MODULE_PAYMENT_PAYPAL3_ALLOWED_DESC',
        ];
        foreach ($constant_names as $constant_name) {
            defined($constant_name) or define($constant_name, $this->text->get_text($constant_name));
        }
    }
    
}

MainFactory::load_origin_class('paypal3');
